package org.hawk.gwt.ppc;

import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.net.URL;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;

import org.hawk.gwt.ppc.introspection.IntrospectionException;
import org.hawk.gwt.ppc.introspection.IntrospectionUtils;
import org.hawk.gwt.ppc.introspection.SourcesRepository;
import org.hawk.gwt.ppc.loader.FileSystemSourcesLoader;
import org.hawk.gwt.ppc.loader.ProcessingUnit;
import org.hawk.gwt.ppc.loader.SourcesLoader;
import org.hawk.gwt.ppc.loader.UniversalSourcesLoader;
import org.hawk.gwt.ppc.parser.ClassDescriptor;
import org.hawk.gwt.ppc.parser.CompilationUnitDescriptor;
import org.hawk.gwt.ppc.parser.PreprocessorIOException;
import org.hawk.gwt.ppc.parser.Token;
import org.hawk.gwt.ppc.utils.FileSystem;
import org.hawk.gwt.ppc.utils.Name;
import org.hawk.gwt.ppc.utils.PreprocessorException;

/**
 * Facade class to do all the preprocessing jobs.
 * Delegates all work to corresponding classes.
 * 
 * @author alex.bereznevatiy@gmail.com
 */
public class ProcessingUnitsManager {
	private static final String GWT_MODULE_EXTENSION = ".gwt.xml";
	private static final String PPC_WORKING_DIR = "gwt-ppc";
	
	private static ProcessingUnitsManager instance;
	private ProcessingUnitsManager(){}
	
	private Environment env = Environment.getInstance();
	private Configuration conf;
	private File work;
	private SourcesLoader loader;
	private SourcesLoader workLoader;
	private final LinkedList<ProcessingUnit> processors = new LinkedList<ProcessingUnit>();
	private Set<String> loadedModules;
	private SourcesRepository repository;

	public static ProcessingUnitsManager getInstance() {
		if(instance==null){
			instance = new ProcessingUnitsManager();
		}
		return instance;
	}
	
	/**
	 * @return sources loader used to resolve classes sources.
	 */
	public SourcesLoader getSourcesLoader() {
		return loader;
	}
	
	/**
	 * Adds specific preprocessor to this manager.
	 * @param preprocessor to add
	 */
	void addProcessingUnit(ProcessingUnit ppc) {
		processors.add(ppc);
		ppc.configure(conf, this);
	}
	
	/**
	 * Invokes actual preprocessing of java code for 
	 * the whole classpath.
	 * @throws PreprocessorException
	 */
	void run() throws PreprocessorException {
		Log.info("Preprocessing java code");
		loadedModules = new HashSet<String>();
		iterateModules(env.getConfiguration().getModules());
		flushChangedClasses();
		Log.info("Preprocessing has been finished");
	}
	
	private void flushChangedClasses() throws PreprocessorException {
		Set<CompilationUnitDescriptor> processed = new HashSet<CompilationUnitDescriptor>();
		for (String clazzName : repository.getChangedClasses()) {
			ClassDescriptor clazz = repository.requireClass(clazzName);
			CompilationUnitDescriptor compilationUnit = clazz.getCompilationUnit();
			flushChangedCU(processed, compilationUnit);
		}
	}

	private void flushChangedCU(Set<CompilationUnitDescriptor> processed,
			CompilationUnitDescriptor compilationUnit) throws PreprocessorIOException {
		if (processed.contains(compilationUnit)) {
			return;
		}
		processed.add(compilationUnit);
		Token tok = compilationUnit.getFirstToken();
		
		// TODO: it is possible to optimize this: 
		// 1) with common array for all classes
		// 2) with multiple threads
		// idea: FlushThead that combines roles of StringBuilder and Thread.
		StringBuilder result = new StringBuilder();
		
		while (tok != null) {
			result.append(tok.getRaw());
			tok = tok.next();
		}
		
		try {
			FileSystem.write(new File(work, Name.javaToFs(compilationUnit.getName())), new StringReader(result.toString()));
		} catch (IOException ex) {
			throw new PreprocessorIOException(ex);
		}
	}

	/**
	 * @return loader for all the processed sources.
	 */
	SourcesLoader getWorkLoader() {
		return workLoader;
	}
	
	/**
	 * Releases all resources taken from the system.
	 */
	void dispose() {
		loadedModules = null;
		FileSystem.markForDeletion(work);
	}
	
	/**
	 * Configures {@link ProcessingUnitsManager} to be able to run.
	 * @throws IOException
	 */
	public void configure() throws IOException {
		conf = env.getConfiguration();
		createWorkDir();
		loader = new UniversalSourcesLoader();
		workLoader = new FileSystemSourcesLoader(work);
		repository = IntrospectionUtils.getRepository(loader);
	}
	
	/**
	 * @return working directory where results of preprocessing are stored.
	 */
	public File getWorkDir() {
		return work;
	}
	
	/**
	 * @return sources repository associated with loader returned by {@link #getSourcesLoader()}.
	 */
	public SourcesRepository getRepository() {
		return repository;
	}
	
	private void createWorkDir() {
		String war = conf.get(Configuration.WAR_DIR_PARAMETER, System.getProperty("java.io.tmpdir"));
		if (FileSystem.exists(war, PPC_WORKING_DIR)) {
			FileSystem.rm(war, PPC_WORKING_DIR);
		}
		work = FileSystem.mkdir(war, PPC_WORKING_DIR);
	}
	
	private void iterateModules(String[] modules) throws PreprocessorException {
		for(String name : modules) {
			processModule(name);
		}
	}
	
	void processModule(String name) throws PreprocessorException {
		if (loadedModules == null || loadedModules.contains(name)) {
			return;
		}
		loadedModules.add(name);
		URL moduleUrl = loader.getResource(
				Name.javaToFs(name, GWT_MODULE_EXTENSION));
		if (moduleUrl == null) {
			throw new PreprocessorException("Unable to find descriptor for module: " + name);
		}
		GWTModule module = new GWTModule(name, moduleUrl);
		iterateModules(module.getInheritedModules());
		processModule(module);
		
	}
	
	private void processModule(GWTModule module) throws PreprocessorException {
		if (!hasAtLeastOneResourceToPreprocess(module)) {
			Log.debug("\tNothing to process for module: " + module);
			return;
		}
		Log.info("   Processing module "+module.getName());
		
		processModule(module.getClientPackages());
		processEntryPoints(module);
		processModuleDescriptor(module);
	}
	
	private void processEntryPoints(GWTModule module) throws PreprocessorException {
		for(String entryPoint : module.getEntryPoints()) {
			try {
				processEntryPoint(entryPoint);
			} catch(IOException ex) {
				throw new PreprocessorException();
			}
		}
	}
	
	private void processEntryPoint(String entryPoint) throws PreprocessorException, IOException {
		if (processors.isEmpty()) {
			return;
		}
		for (ProcessingUnit ppc : processors) {
			ppc.processEntryPoint(entryPoint);
		}
	}
	
	private void processModuleDescriptor(GWTModule module) throws PreprocessorException {
		for(ProcessingUnit ppc : processors) {
			ppc.processModule(module);
		}
		try {
			File result = new File(work, Name.javaToFs(module.getName(), GWT_MODULE_EXTENSION));
			FileSystem.write(result, module.toStream());
		} catch (IOException ex) {
			throw new PreprocessorException(ex);
		}
	}
	
	private void processModule(String[] clientPackages) throws PreprocessorException {
		for(String pkg : clientPackages){
			processPackage(pkg);
		}
	}
	
	private void processPackage(String pkg) throws PreprocessorException {
		for(String clazz : loader.getCompilationUnits(pkg)){
			processCompilationUnit(clazz);
		}
		processModule(loader.getSubpackages(pkg).toArray(new String[0]));
	}
	
	private void processCompilationUnit(String clazz) throws PreprocessorException {
		if (repository.isChanged(clazz)) {
			return;
		}
		for (ProcessingUnit p : processors) {
			if(p.acceptResource(clazz)) {
				try {
					p.processCompilationUnit(clazz);
				} catch (PreprocessorException ex) {
					Log.error("Unable to process compilation unit: " + clazz, ex);
				} catch (IntrospectionException ex) {
					Log.error("Unable to process compilation unit: " + clazz, ex);
				} catch (Throwable th) {
					Log.error("Unable to process compilation unit: " + clazz, th);
				}
			}
		}
	}
	
	private boolean hasAtLeastOneResourceToPreprocess(GWTModule module) {
		return hasAtLeastOneResourceToPreprocess(module.getClientPackages());
	}
	
	private boolean hasAtLeastOneResourceToPreprocess(String[] packages){
		for(String pkg : packages){
			if (hasAtLeastOneResourceToPreprocess(pkg) || 
					hasAtLeastOneResourceToPreprocess(
							loader.getSubpackages(pkg).toArray(new String[0]))) {
				return true;
			}
		}
		return false;
	}
	
	private boolean hasAtLeastOneResourceToPreprocess(String pkg) {
		for(String cls : loader.getCompilationUnits(pkg)) {
			for (ProcessingUnit p : processors) {
				if(p.acceptResource(cls)) {
					return true;
				}
			}
		}
		return false;
	}
}
